在training.ti.com上可以观看调试TI-RTOS常见应用程序问题的视频。
CC2640R2F平台支持cJTAG(2线)和JTAG(4线)接口。任何支持cJTAG的调试器,如TI XDS100v3和XDS200,都可以原生工作。其他接口,如IAR I-Jet和Segger J-Link,只能在JTAG模式下使用,但是驱动程序会注入一个cJTAG序列,保证在连接时可以进行JTAG模式。用于调试的设备上包含的硬件资源如下。调试器和IDE的所有组合不是所有的调试功能都可用。
SmartRF06板包含XDS100v3调试探针,CC2650 LaunchPad包含XDS110调试探针。默认情况下,这些调试器在相应的示例项目中使用。
如果仅连接一个调试器,则当您单击CCS中的或IAR中的中的按钮时,IDE会自动使用它。
如果连接了多个调试器,则必须选择要使用的各自调试器。以下步骤详细介绍如何在CCS和IAR中选择调试器。
要在CCS中选择特定的调试器,请执行以下操作:
要查找XDS100v3调试器的序列号,请执行以下操作:
C:\ti\ccsv6\ccs_base\common\uscif\xds100serial.exe
以获取连接的调试器的序列号列表。对于XDS110调试器(LaunchPads),请运行以下命令: c:\ti\ccsv6\ccs_base\common\uscif\xds110\xdsdfu.exe -e
要使用IAR进行调试,请执行以下操作:
--drv_communication=USB:#select
添加此命令行选项使IAR提示哪个调试器用于每个连接。
IAR和CCS都保留了一个步进等效指令比较器。五个硬件断点可用于调试。本节介绍在IAR和CCS中设置断点。
要切换断点,请执行以下任何操作。
有关活动和非活动断点的概述,请单击“View” - >“Breakpoints”。
要设置条件中断,请执行以下操作。
调试时,跳过计数和条件可以帮助跳过多个断点,或者只有在变量为特定值时才会中断。
注意:
条件中断需要调试器响应,并且可能会使处理器长时间停止,以断开有源RF连接,否则会中断调试目标上的时序。
要切换断点,请执行以下任何操作:
断点如下所示:
图101. 断点PIN_init()
。调试器在main()
的开头停止。
有关活动和非活动断点的概述,请单击View->Breakpoints。
图102. 断点列表。右键单击以编辑选项,或取消选择以停用。
要设置条件中断,请执行以下操作。
调试时,跳过计数和条件可以帮助跳过多个断点,或者只有在变量为特定值时才会中断。
注意:
条件中断需要调试器响应,并且可能会使处理器长时间停止,以断开有源RF连接,否则会中断调试目标上的时序。
同步RF协议(如蓝牙低功耗协议)是时序敏感的,断点可以很容易地停止执行足够长的时间,从而失去网络时序并破坏链路。
为了仍然能够进行调试,将断点尽可能靠近可以读取相关调试信息的位置,或者通过相关代码段进行调试。
在您打破断点并读出必要的调试信息后,建议您重置设备并重新建立连接。
当启用编译器优化时,在C代码行上切换断点可能不会导致预期的行为。一些例子包括以下内容。
代码被删除或未被编译
在IDE中切换断点会导致断点不在选定行上。某些IDE在非现有代码上禁用断点。
代码块是公共子表达式的一部分
例如,断点可能会从另一个函数调用的函数内部切换,但也可能由于来自另一个无意的函数的调用而中断。
if子句由汇编中的条件分支表示
if子句中的断点在条件语句中总是中断,即使不执行。
TI建议在调试时选择尽可能低的优化级别。有关修改优化级别的信息,请参阅Optimizations
。
IAR和CCS提供了几种查看暂停程序状态的方法。全局变量在链接时静态放置,并且可以在项目中可用的RAM中的任何位置,或者如果它们被声明为恒定值,则可能在Flash中。可以随时通过Watch和Expression窗口访问这些变量。
除非由于优化而被移除,否则这些视图中总是可以使用全局变量。仅在有限范围内有效的局部变量或变量将放置在活跃任务的堆栈中。也可以使用Watch或Expression视图来查看这些变量,但也可以在打破或逐步执行代码时自动显示。要通过IAR和CCS查看变量,请执行以下操作。
您可以通过执行以下任一操作来查看全局变量。
图103. 可变观察窗口。请注意,您可以转换值,获取地址和大小等。
要查看全局变量,请执行以下任一操作。
View –> Locals 显示IAR中的局部变量。
图106. 局部变量。该截图是在执行简单外设初始化功能时执行的。
局部变量通常放在CPU寄存器中,而不是堆栈。这些变量也具有有限的使用寿命,即使在有效的范围内,这取决于所执行的优化。由于CCS和IAR的寿命有限,CCS和IAR都可能难以显示特定的变量。调试时的解决方法如下。
IAR可以在优化过程中删除该变量,并内联该值的使用。如果是这样,请在前面添加__root
指令。
如Debug Interfaces
中所述,DWT模块包含四个内存观察点,允许内存访问断点。硬件匹配功能仅在地址上。如果要用于变量,变量必须是全局变量。在IAR和CCS中使用观察点如下所述。
注意:
如果使用值匹配的数据观察点,则使用四个观察点中的两个。
此示例配置确保如果将0x42写入蓝牙低功耗简单外设示例项目中的特征1的存储位置,则设备将停止执行。
图108. 配置硬件观察点以打破值为0x42的8位写入。
Set Data Breakpoint for 'myVar'
将其添加到活动断点。Edit...
以设置观察点是否应该在读取,写入或任何访问上匹配。IAR和CCS都包括RTOS对象查看器(ROV)插件,可以深入了解TI-RTOS的当前状态,包括任务状态,堆栈等。因为CCS和IAR都有类似的接口,所以这些例子只讨论CCS。
访问IAR中的ROV:
访问CCS中的ROV:
本节讨论一些对调试和分析有用的ROV视图。更多详细信息,请参见“TI-RTOS用户指南”,包括有关如何将日志事件添加到应用程序代码的文档。
BIOSScan for errors
通过可用的ROV模块浏览并报告错误。如果出现任何问题,这个功能可以是一个很好的开始。此扫描仅显示与TI-RTOS模块相关的错误,并仅显示可捕获的错误。
图110. 扫描错误。这里有一个任务堆栈被超载。
任务 Detailed
视图对于查看每个任务的状态及其运行时相关的堆栈使用情况非常有用。此示例显示第一次调用用户线程时的状态。图111示出了由其ICall代理,空闲任务,simple_peripheral任务和GAPRole任务展示的蓝牙低功耗堆栈任务。
图111. 任务的详细视图。请注意,溢出任务的地址与扫描的错误匹配实例ID。
以下列表说明了图111中的条列。
address
此列显示每个任务的Task_Struct实例的内存位置。
priority
此列显示任务的TI-RTOS优先级。
mode
此列显示任务的当前状态。
fxn
此列显示任务的输入功能的名称。
arg0,arg1
这两列显示了可以赋予任务的入口函数的清晰值。在图像中,ICall_taskEntry为0xb001,它是RF堆栈图像的入口函数的Flash位置和0x20003a30(在main()中定义的bleUserCfg_t user0Cfg的位置)。
stackPeak
该列显示了基于RAM中起始位置运行时使用的最大堆栈内存,其中堆栈以0xBE预填充,并且在运行时堆栈的末尾有一个哨兵字。
注意:
函数调用可能会将堆栈指针推出运行时堆栈,但实际上并不写入整个区域。stackSize附近的堆栈峰值不超过它可能表示堆栈溢出。
stackSize
此列显示在实例化任务时配置的运行时堆栈的大小。
stackBase
此列显示任务的运行时堆栈的逻辑顶部(用法从stackBase+stackSize开始,并且下降到该地址)。
Hwi Module
视图允许在boot或main(),Hwi执行和Swi执行期间用来对系统堆栈进行分析。有关系统堆栈的更多信息,请参阅System Stack
。
图112.在Hwi中查看系统堆栈
hwiStackPeak,hwiStackSize和hwiStackBase可用于检查系统堆栈是否溢出。
IAR和CCS都能够显示设备上的内存表示。在CCS中,您可以按地址或符号名称进行索引。例如,考虑图111中堆栈的溢出:
Simple Peripheral
Task的堆栈。注意BE
水印
GAPRole
Task的堆栈。注意它已经完全填满了。
在这种情况下,解决方案是增加失败任务的堆栈大小,并查看堆栈的顶点是什么。该stackPeak
报告是依靠许多水印字节覆盖,所以不能知道超限达多少。
因为堆栈从较高的地址到较低的寻址(在图像中向上)使用,所以超过的堆栈将倾向于在堆栈之前的位置覆盖数据。
如Dynamic Memory Allocation
(动态内存分配)中所述,ICall堆管理器及其堆用于在蓝牙低功耗堆栈任务和应用程序任务之间分配消息,并作为任务中的动态内存分配。
为ICall堆提供性能分析功能,但默认情况下不启用。因此,必须通过添加HEAPMGR_METRICS
到定义的预处理器符号来编译它 。此功能可用于查找不明原因行为的潜在来源,并优化堆的大小。当HEAPMGR_METRICS
被定义,如下变量和函数变得可用。全局变量:
heapmgrBlkMax
同时分配块的最大数量
heapmgrBlkCnt
当前分配块的数量
heapmgrBlkFree
当前的空闲块数量
heapmgrMemAlo
当前总内存分配以字节为单位
heapmgrMemMax
块中同时分配的内存的最大数量(此值不能超过堆的大小)
heapmgrMemUb
被分配的块的最远的存储位置,被测量为从堆的开始的偏移量
heapmgrMemFail
内存分配失败的数量(ICall_malloc()
已返回NULL的实例)
void ICall_heapGetMetrics(u16 *pBlkMax, u16 *pBlkCnt, u16 *pBlkFree, u16 *pMemAlo, u16 *pMemMax, u16 *pMemUb)
返回前面描述的作为参数传递的指针中的变量
int heapmgrSanityCheck(void)
如果堆是正常的则返回0; 否则返回非零(即数组访问已经溢出)
当启用自动堆大小功能(HEAPMGR_SIZE=0
)时,以下过程可用于查看ICall堆的大小。
在运行状态中,当main()中的ICall_init()
执行后,查看全局内存符号HEAPMGR_SIZE
的值。HEAPMGR_SIZE
的值是ICALL堆的总大小。请参阅heapmgr.h中的HEAPMGR_INIT()
来查看源代码实现。
HEAPMGR_SIZE
HEAPMGR_SIZE
注意:
自动堆大小功能不能确定应用程序所需的堆数量。系统设计人员必须确保堆具有满足应用程序运行时内存需求所需的空间。
由于内存放置,实际的堆大小可能最多减少4个字节。
通过检查IAR中的应用程序的map文件来计算ICall堆的大小:
ICall堆的大小是.bss部分中最后一个项目的地址与系统堆栈(CSTACK)的起始地址之间的区别。例如,simple_peripheral\cc2650lp\app.map
文件中在PLACEMENT SUMMARY结尾处的部分:
清单94.bss结束和CSTACK开始之间的空间用于ICall堆。
.bss zero 0x20001cf8 0x2 app_ble_prm3.orm3 [2]
.bss zero 0x20001cfa 0x1 TRNGCC26XX.o [1]
.bss zero 0x20001cfb 0x1 driverlib_release.o [8]
- 0x20001cfc 0x1af4
“A3”: 0x400
CSTACK 0x20003f50 0x400 <Block>
CSTACK uninit 0x20003f50 0x400 <Block tail>
- 0x20004350 0x400
该示例中的ICall堆的大小是CSTACK开始的地址减去.bss中的最后一个项目的地址或堆的地址。0x20003f50 - 0x20001cfc = 0x2254
即8788bytes
通过检查CCS中的应用程序map文件来计算ICall堆的大小:
堆的大小由链接器发出的heapStart和heapEnd全局符号地址决定。例如,simple_peripheral_cc2650lp_app.map
文件:
清单95. 在CCS中的ICall堆是使用了heapStart end和heapEnd start之间的空间。
20003f48 heapEnd
20001cc1 heapStart
此示例中的ICall堆的大小定义为: 0x20003f48 - 0x20001cc1 = 0x2287
即8839 bytes
调试时,关闭或降低优化,以缓解单步执行代码。可以在项目,文件和功能级别配置优化级别。
对于调试,理想情况下,优化的项目范围设置应尽可能的低。目标可能没有足够的可用空间来执行此操作,但降低几个级别可能会有所帮助。
在IAR中
Project Options –> C/C++ Compiler –> Optimizations
图113.IAR中的项目级优化设置
在CCS中
Project Properties –> CCS Build –> ARM Compiler –> Optimization
图114. CCS中的项目级优化设置
注意:
进行单文件优化时要小心,因为这也会覆盖整个项目范围的预处理符号。
在IAR
在CCS中
使用编译器指令,您可以控制单个函数的优化级别。
在IAR
在函数定义之前使用#pragma optimize = none
来优化整个函数,也就是如下。
清单96. IAR中的函数级优化设置
#pragma optimize = none
static void myFunction(int number )
{
// ...
return yourFunction(other_number );
}
在CCS中
清单97. CCS中的函数级优化设置
#pragma FUNCTION_OPTIONS(myFunction,“--opt_level = 0”)
static void myFunction (int number )
{
// ...
return yourFunction (other_number );
}
一些TI-RTOS内核模块包含在ROM中,并从ROM执行,以节省应用程序的Flash空间。当反汇编视图和调用堆栈视图中只显示地址时,可能会导致一些混淆。
ROM中所有的TI-RTOS内核代码都以地址0x1001xxxx
开头。为了了解ROM的代码,您需要在调试会话中包含符号文件。
在IAR
Debugger
的Images
选项中,添加图像<SDK_INSTALL_DIR>\kernel\tirtos\packages\ti\sysbios\rom\cortexm\cc26xx\r2\golden\CC26xx\rtos_rom.xem3
Debug info only
框,并使Offset = 0
。在CCS中
图116. 通过添加符号在ROM中添加BIOS内核的符号信息。
<SDK_INSTALL_DIR>\kernel\tirtos\packages\ti\sysbios\rom\cortexm\cc26xx\r2\golden\CC26xx\rtos_rom.xem3
存在几种可能的异常原因。如果捕获到异常,则可以调用异常处理函数。根据项目设置,这个处理程序可能是ROM中的一个默认处理程序,它只是一个无限循环,或者是从这个默认处理程序调用的自定义函数而不是一个循环。
发生异常时,根据调试器的不同,在调试模式下该异常可能会被立即捕获并暂停。如果稍后通过断点调试器手动停止执行,则它将在异常处理程序循环中停止。
使用TI-RTOS的默认设置,可以在寄存器CFSR
(Configurable Fault Status Register)中的System Control Space
寄存器组(CPU_SCS)中找到异常原因。ARM Cortex-M3用户指南
介绍了该寄存器。大多数异常原因分为以下三类。
CFSR
寄存器可以在IAR和CCS的View->Registers下查看。当发生访问冲突时,异常类型为IMPRECISERR
,因为对Flash和外围存储器区域的写入大多是缓冲写入。
如果CFSR:BFARVALID
标志在异常发生时被设置(典型的是PRECISER
R),则可以读取CPU_SCS
中的BFAR
寄存器,以找出引起异常的内存地址。
如果异常是IMPRECISERR
,PRECISERR
可以通过手动禁用缓冲写入来强制执行。将[CPU_SCS:ACTRL:DISDEFWBUF]
设置为1,可以通过手动设置能在IAR/CCS中查看的寄存器的的位,或者包含Driverlib
中<hw_cpu_scs.h>
并调用以下命令。
#include<ti/devices/cc26x0r2/inc/hw_cpu_scs.h>
// ..
int main ()
{
// Disable write-buffering. Note that this negatively affect performance.
HWREG (CPU_SCS_BASE + CPU_SCS_O_ACTLR )= CPU_SCS_ACTLR_DISDEFWBUF ;
// ..
}
要在RTOS Object View
(ROV)中启用异常解码而不使用太多内存,请使用TI-RTOS中的最小异常处理程序。BLE5-Stack工程中的默认选择是不使用异常处理程序。
要进行设置,请更改与M3Hwi相关的TI-RTOS配置文件的部分,使其类似于以下代码:
//m3Hwi.enableException = true;
m3Hwi.enableException = false ;
//m3Hwi.excHandlerFunc = null;
m3Hwi.excHookFunc = “&execHandlerHook” ;
然后,在某些地方做一个功能签名void (*Hwi_ExceptionHookFuncPtr)(Hwi_ExcContext*)
像下面的这段代码一样:
#include <ti/sysbios/family/arm/m3/Hwi.h>
// ...
volatile uintptr_t * excPC = 0 ;
volatile uintptr_t * excCaller = 0 ;
// ...
void execHandlerHook(Hwi_ExcContext * ctx )
{
excPC=ctx-> pc ; // Program counter where exception occurred
excCaller=ctx-> lr ; //Link Register when exception occurred
while(2 );
}
设置m3Hwi.enableException
为false
可以使填满ROV查看的全局Hwi_ExcContext结构的最小处理程序来显示已解码的异常。通过设置excHookFunc
,最小异常处理程序将调用此函数并传递一个指向异常上下文的指针,供用户使用。此结构在<ti/sysbios/family/arm/m3/Hwi.h>
中定义。
当发生异常时,设备应该以无限循环结束。检查ROV->Hwi->Exception information(异常信息)
。
图117. 解码异常,故意写入地址0x0013是非法的。请注意,禁止写缓冲区以获取精确的错误位置,并且m3Hwi.enableException
已设置为false
以获得解码。
在这种情况下,在函数writeToAddress中通过取消引用地址0x0013并尝试写入它来使总线故障被强制执行:
清单98. 写入FLASH存储器区域中的一个地址。
void writeToAddress (uintptr_t *addr,int val){
*(int*)addr = val;
}
// ..
void taskFxn (...){
// ..
writeToAddress((void* )19,4);//Randomly chosen values
}
写指令放在application.c
的第79行,如图所示。要想获得精确的位置,如前所述写入缓冲区是禁止的。
查看PC(程序计数器)和LR(链接寄存器)指定的位置的反汇编视图可能是有启发性的。PC是假定的异常位置,LR通常是故障功能应该返回的位置。例如,在这个异常中的PC:
图118. 这里pc
从反汇编视图中查找了解码的异常。
这里需要一些取证。我们从ROV中的Hwi解码(和异常钩子中的异常上下文)得到当异常发生时程序计数器是0x708e
。
在该位置有一个存储指令str r0, [r1]
,意思是将R0中的值存储到R1中的存储器地址。上图中SP
的事务与优化被关闭有关,因此所有局部变量都存储在堆栈中,即使在这种情况下,R0和R1也可以直接从调用者使用。
现在我们知道异常发生,因为有人用了无效地址来调用writeToAddress
。
由于有了异常解码器,我们可以通过查看堆栈调用来轻松找到调用站点,但是如果堆栈调用没有帮助,我们可以看看lr
,在异常解码器中看到的是0x198f
图119 调用站点中指定lr
。请注意,lr
是调用writeToAddress
后的指令,因为执行将在此恢复。
我们可以看到,R0和R1用常量初始化。这意味着某些程序员有意地用一个地址来调用write function
导致总线故障的发生。
通常,总线故障的原因是指针没有被初始化,并且类似writeToAddress
这样的函数获得指针,假设它是有效的,然后引用指针就会写入无效的址。
本节介绍如何调试程序运行内存不足的情况,无论是在堆上还是在单个线程上下文的运行时堆栈上。数组越界或为结构体动态分配太少内存会破坏内存,并可能导致异常,如INVPC,INVSTATE,IBUSERR出现在CFSR寄存器中。
如果任务的运行时堆栈或系统堆栈发生溢出(使用ROV插件发现),请执行以下步骤。
Initializing a Task
(初始化任务)和System Stack
(系统栈)中所述Profiling the ICall Heap Manager (heapmgr.h)
(分析ICall堆管理器(heapmgr.h))介绍如何使用ICall堆分析功能。要检查是否发生动态分配错误,请执行以下操作:
heapmgrMemAlo
或heapmgrMemAlo
时候接近HEAPMGR_SIZE
memFail
以查看是否发生了分配故障。HEAPMGR_SIZE
并查看问题是否仍然存在。您可以在heapmgr.h
中的HEAPMGR_MALLOC()
函数的hdr = NULL
行设置一个断点,以找到失败的分配。
表22.列出了simple_peripheral
项目中应用程序使用的预处理程序符号。必须把未修改的符号在Modify
列中标记为N
,修改的符号在Modify
列中标记为Y
。
表23.列出了可能被修改的唯一堆栈预处理器选项:
应用程序和堆栈项目都会生成一个map文件,可以用于计算Flash和RAM组合的内存使用情况。两个项目都有自己的内存空间,我们必须分析两个map文件来确定系统内存的总体使用情况。map文件位于IAR中相应项目的输出文件夹中。要计算总内存使用量,请执行以下操作。
注意:
在文件末尾,三行包含read-only code
,read-only data
和read/write data
的内存使用情况。
read-only code
,read-only data
内存的值加起来。注意:
这个总和是应用程序项目总Flash使用量。read/write data
存储器是应用程序项目的总RAM使用量。
对于CCS,各自项目的map文件提供了Flash和RAM使用情况的总和。要确定每个项目的剩余可用内存,请参阅Flash
和RAM
。由于部分放置和对齐要求,某些剩余的内存可能不可用。只有工程构建和链接成功后,map文件中的内存使用情况才有效。
一个程序永远不会退出main()
函数,通常导致它退出的原因是某些软件模块调用了abort()
。
当这种情况发生时,IAR和CCS都会停止执行,反汇编和调用堆栈将显示某些类型的__exit
符号。
当以下情况发生时,BLE协议栈会调用ICall_abort()
:
ICall_abort
函数中设置断点以跟踪此错误的发生。断言在调试时很有用,可以在代码中捕获不良状态。BLE堆栈项目默认设置将EXT_HAL_ASSERT
全局预处理器符号启用,这将尝试调用用户应用程序可以定义的断言处理程序。
应用程序有一个断言回调来捕获堆栈项目中的断言。断言回调在每个工程的main()函数中注册:
/*Register Application callback to trap asserts raised in the Stack*/
RegisterAssertCback(AssertHandler);
main.c
文件也包含一个示例AssertHandler
函数。
可在回调返回一些通用的断言原因包括HAL_ASSERT_CAUSE_TRUE
, HAL_ASSERT_CAUSE_OUT_OF_MEMORY
,和HAL_ASSERT_CAUSE_ICALL_ABORT
。
当使用分割映像构建配置时,可能会获得HAL_ASSERT_CAUSE_INTERNAL_ERROR
断言。这通常表示ICall bleAPITable
调度表缺少一些函数,因此它调用通用icall_liteErrorFunction
错误处理程序。通常,修复此错误是使缺少的预定义编译器选项能够获取正确的API bleAPITable
。
用户可以决定如何在回调中处理这些断言。默认情况下,它将进入大多数断言的自旋锁。
断言还可以定义一个给定断言的更具体原因的子句。子句的一个例子是HAL_ASSERT_OUT_OF_HEAP
描述导致断言的内存的类型HAL_ASSERT_CAUSE_OUT_OF_MEMORY
。
如果没有注册应用程序回调,则调用默认的assert回调函数,并返回,而不需要进一步的操作,除非HAL_ASSERT_SPIN
在应用程序项目中定义了这个回调 ,这个应用程序在无限循环中陷阱。另外,如果堆栈项目没有被捕获到应用程序回调中,也可以定义以下之一:
HAL_ASSERT_RESET
:重置设备HAL_ASSERT_LIGHTS
:打开危险指示灯(由用户配置)HAL_ASSERT_SPIN
:无限期地旋转一个循环有关实现细节,请参阅堆栈工程中的hal_assert.h
和hal_assert.c
。
文章所有代码、工具、文档开源。加入我们QQ群 591679055获取更多支持,共同研究CC2640R2F&BLE5.0。